package com.ztesoft.book.api.controller.sys;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SmUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.ztesoft.book.common.constant.Constant;
import com.ztesoft.book.common.enums.IntEnum;
import com.ztesoft.book.common.utils.Result;
import com.ztesoft.book.common.utils.encrypt.AesEncryptSm4Util;
import com.ztesoft.book.common.utils.exception.ExUtil;
import com.ztesoft.book.common.utils.exception.ServiceException;
import com.ztesoft.book.common.utils.jwt.JwtUtil;
import com.ztesoft.book.common.utils.redis.RedisUtils;
import com.ztesoft.book.core.base.base.enums.YesNoEnum;
import com.ztesoft.book.core.sys.loginmobile.dto.BindUserInfoDTO;
import com.ztesoft.book.core.sys.loginmobile.qo.BindUserInfoByLoginName;
import com.ztesoft.book.core.sys.sysdict.dto.SysDictDTO;
import com.ztesoft.book.core.sys.sysdict.service.SysDictService;
import com.ztesoft.book.core.sys.sysuser.dto.SysUserDTO;
import com.ztesoft.book.core.sys.sysuser.qo.SysUserLoginQO;
import com.ztesoft.book.core.sys.sysuser.service.SysUserService;
import com.ztesoft.book.core.wx.wxbinduser.dto.WxBindUserDTO;
import com.ztesoft.book.core.wx.wxbinduser.service.WxBindUserService;
import com.ztesoft.book.core.wx.wxminuser.dto.WxMinUserDTO;
import com.ztesoft.book.core.wx.wxminuser.qo.WxMinUserQO;
import com.ztesoft.book.core.wx.wxminuser.service.WxMinUserService;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author lzq.
 * @description 移动端登录con
 * @date 2021-09-26 16:01
 */
@Slf4j
@RestController
@RequestMapping("/sys/login")
public class LoginController {

    @Resource
    private SysUserService sysUserService;

    @Resource
    private WxBindUserService wxBindUserService;

    @Resource
    private WxMinUserService wxMinUserService;

    @Resource
    private SysDictService sysDictService;

    private static String redisTokenExpireTime;

    @Value("${book.time.redis-token-expire}")
    public void setRedisTokenExpireTime(String redisTokenExpireTime1) {
        setRedisTokenExpireTimeValue(redisTokenExpireTime1);
    }

    public static void setRedisTokenExpireTimeValue(String redisTokenExpireTime1) {
        LoginController.redisTokenExpireTime = redisTokenExpireTime1;
    }

    /**
     * 根据openId获取绑定信息（免登）
     *
     * @param bindUserInfoByLoginName bindUserInfoByLoginName
     * @return Result
     */
    @PostMapping("/bindUserByLoginName")
    public @ResponseBody
    Result bindUserByLoginName(@RequestBody @Validated BindUserInfoByLoginName bindUserInfoByLoginName) {
        try {
            ExUtil.checkNotNull(bindUserInfoByLoginName, "参数不能为空");
            ExUtil.checkNotNull(bindUserInfoByLoginName.getOpenid(), "openid不能为空");
            // 对传入的用户名进行ext加密，再与数据库比对
            LambdaQueryWrapper<SysUserDTO> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(SysUserDTO::getUsername, AesEncryptSm4Util.encrypt(bindUserInfoByLoginName.getUsername()))
                    .or()
                    .eq(SysUserDTO::getMobile, AesEncryptSm4Util.encrypt(bindUserInfoByLoginName.getUsername()))
                    .last("limit 1");
            SysUserDTO userDtoTemp = sysUserService.getBaseMapper().selectOne(queryWrapper);
            if (userDtoTemp == null) {
                return Result.error("用户名或密码错误");
            }
            // 参数及用户信息校验
            checkUserLogin(userDtoTemp);
            UpdateWrapper<SysUserDTO> wrapper = new UpdateWrapper<>();
            // 密码 + 用户名进行国密SM3加密，再与数据库比对
            String key = SmUtil.sm3(userDtoTemp.getSalt() + bindUserInfoByLoginName.getPassword());
            if (key.equals(userDtoTemp.getPassword())) {
                // 清除锁定状态并绑定
                SysUserLoginQO sysUserLoginQO = clearLockAndBind(userDtoTemp, wrapper, bindUserInfoByLoginName.getOpenid());
                // 处理用户信息（redis、返回值等）
                BindUserInfoDTO bindUserInfoDTO = getBindUserInfoDTO(sysUserLoginQO, userDtoTemp.getId(),
                        bindUserInfoByLoginName.getOpenid(), userDtoTemp.getNickName(),
                        userDtoTemp.getLogo());
                WxMinUserQO wxMinUserQO = new WxMinUserQO();
                wxMinUserQO.setMinOpenid(bindUserInfoByLoginName.getOpenid());
                WxMinUserDTO wxUser = wxMinUserService.findOne(wxMinUserQO);
                if (ObjectUtil.isNotNull(wxUser)) {
                    sysUserLoginQO.setWxAvatar(wxUser.getCover());
                }
                return Result.success("移动端绑定成功", bindUserInfoDTO);
            }
            else {
                // 密码输入错误上限5次,达到上限次数锁定账号5分钟
                setErrorPwdInfo(userDtoTemp, wrapper);
                return Result.error("绑定账号失败");
            }
        }
        catch (ServiceException se) {
            log.error("绑定账号失败", se);
            return Result.error(se.getMsg());
        }
        catch (Exception e) {
            log.error("绑定账号失败", e);
            return Result.error("绑定账号失败");
        }
    }

    /**
     * 方法功能描述:
     * < 参数及用户信息校验 >
     *
     * @param userDtoTemp 参数说明
     * @return void
     * @author YangFan
     * @date 2022-01-14 16:43
     */
    private void checkUserLogin(SysUserDTO userDtoTemp) {
        ExUtil.checkNotNull(userDtoTemp, "该帐号不存在");
        ExUtil.checkArgument(YesNoEnum.NO.getValue().equals(userDtoTemp.getStatus()), "该帐号已停用，请联系管理员");
        ExUtil.checkArgument(Objects.isNull(userDtoTemp.getUserLockTime()) || LocalDateTime.now()
                .isAfter(userDtoTemp.getUserLockTime()), "该帐号已锁定，请联系管理员解锁");
    }

    /**
     * 清除锁定状态并绑定
     *
     * @param userDtoTemp
     * @param wrapper
     * @param openid
     * @return
     */
    @NotNull
    private SysUserLoginQO clearLockAndBind(SysUserDTO userDtoTemp, UpdateWrapper<SysUserDTO> wrapper, String openid) {
        // 清除锁定次数和时间
        wrapper.lambda()
                .eq(SysUserDTO::getId, userDtoTemp.getId())
                .set(SysUserDTO::getWrongPwdTimes, IntEnum.ZERO.getValue())
                .set(false, SysUserDTO::getUserLockTime, null);
        sysUserService.update(wrapper);
        // TODO:插入绑定表
        WxBindUserDTO wxBindUserDTO = new WxBindUserDTO();
        wxBindUserDTO.setWxMinOpenid(openid);
        wxBindUserDTO.setSysUserId(userDtoTemp.getId());
        wxBindUserService.saveObject(wxBindUserDTO);
        SysUserLoginQO sysUserLoginQO = new SysUserLoginQO();
        BeanUtil.copyProperties(userDtoTemp, sysUserLoginQO);
        if (StrUtil.isNotEmpty(sysUserLoginQO.getMobile())) {
            sysUserLoginQO.setMobile(AesEncryptSm4Util.desEncrypt(sysUserLoginQO.getMobile()));
        }
        sysUserLoginQO.setIsAdmin(false);
        return sysUserLoginQO;
    }

    /**
     * 方法功能描述:
     * < 获取用户信息 >
     *
     * @param sysUserLoginQO  参数说明
     * @param id              参数说明
     * @param openid          参数说明
     * @param nickName        参数说明
     * @param logo            参数说明
     * @return BindUserInfoDTO
     * @author YangFan
     * @date 2022-01-17 09:51
     */
    @NotNull
    private BindUserInfoDTO getBindUserInfoDTO(SysUserLoginQO sysUserLoginQO, String id, String openid, String nickName, String logo) {
        // 设置RedisToken，时间戳为当前时间戳，并且写入相关登录用户信息，直接设置即可(不用先删后设，会覆盖已有的RedisToken)
        String currentTimeMillis = String.valueOf(System.currentTimeMillis());
        Map<String, Object> redisToken = new HashMap<>(MapUtil.DEFAULT_INITIAL_CAPACITY);
        redisToken.put("currentTimeMillis", currentTimeMillis);
        redisToken.put("user", sysUserLoginQO);
        String userNameStr = AesEncryptSm4Util.desEncrypt(sysUserLoginQO.getUsername());
        RedisUtils.set(Constant.PREFIX_SHIRO_REFRESH_TOKEN + userNameStr, redisToken, Integer.parseInt(redisTokenExpireTime));
        // 从Header中Authorization返回AccessToken，时间戳为当前时间戳
        String token = JwtUtil.signUserExt(id + "", nickName, logo, userNameStr, currentTimeMillis);
        BindUserInfoDTO bindUserInfoDTO = new BindUserInfoDTO();
        bindUserInfoDTO.setIsBind(true);
        bindUserInfoDTO.setToken(token);
        bindUserInfoDTO.setSysUser(sysUserLoginQO);
        bindUserInfoDTO.setOpenid(openid);
        return bindUserInfoDTO;
    }

    /**
     * 方法功能描述:
     * < 密码错误设置出错信息 >
     *
     * @param userDtoTemp 参数说明
     * @param wrapper     参数说明
     * @return void
     * @author YangFan
     * @date 2022-01-17 09:47
     */
    private void setErrorPwdInfo(SysUserDTO userDtoTemp, UpdateWrapper<SysUserDTO> wrapper) {
        int maxWrongPwdTimes = IntEnum.FIVE.getValue();
        String dictId = "2";
        SysDictDTO dict = sysDictService.findById(dictId);
        String lockMinutes = dict.getRemark()
                .trim();
        // 2021-10-08 增加逻辑：上次输错后10分钟再尝试时，重置错误次数后再累计，避免输错4次后，过了很久输错一次直接锁定的不合理情况
        if (userDtoTemp.getUserLockTime() == null) {
            userDtoTemp.setWrongPwdTimes(0);
        }
        else {
            if (LocalDateTime.now().isAfter(userDtoTemp.getUserLockTime().plusMinutes(Long.parseLong(lockMinutes)))) {
                userDtoTemp.setWrongPwdTimes(0);
            }
        }
        int wrongTimes = userDtoTemp.getWrongPwdTimes() + IntEnum.ONE.getValue();
        if (wrongTimes < maxWrongPwdTimes) {
            wrapper.lambda()
                    .eq(SysUserDTO::getId, userDtoTemp.getId())
                    .set(SysUserDTO::getWrongPwdTimes, wrongTimes)
                    .set(SysUserDTO::getUserLockTime, LocalDateTime.now());
            sysUserService.update(wrapper);
            throw new ServiceException(
                    StrUtil.format("用户名或密码错误(Password Error) 剩余输入次数：{}次", maxWrongPwdTimes - wrongTimes));
        }
        else {
            wrapper.lambda()
                    .eq(SysUserDTO::getId, userDtoTemp.getId())
                    .set(SysUserDTO::getWrongPwdTimes, wrongTimes)
                    .set(SysUserDTO::getUserLockTime, LocalDateTime.now()
                            .plusMinutes(Long.parseLong(lockMinutes)));
            sysUserService.update(wrapper);
            throw new ServiceException(StrUtil.format("用户名或密码错误(Password Error) 账户锁定{}分钟", lockMinutes));
        }
    }
}
